from __future__ import annotations

import shutil
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, IO, Iterable, List, Optional, Sequence, Tuple, Union

from ..accounts import load_accounts
from ..events import Event, append_event, read_owner_events
from ..notifications import list_notifications
from ..paths import get_attachments_dir_for, get_home, get_memory_dir
from ..tasks import task_create, task_discuss, task_update_status
from ..util import extract_mentions, parse_iso


@dataclass
class EventDTO:
    id: str
    type: str
    owner: str
    actor: str
    created_at: str
    headers: Dict[str, str]
    content: str
    attachments: List[str]


class ServiceContext:
    """Shared context for web services (memory + attachments roots)."""

    def __init__(self, memory_dir: Optional[Path] = None, attachments_root: Optional[Path] = None):
        self.memory_dir = Path(memory_dir or get_memory_dir())
        self.attachments_root = Path(attachments_root or (get_home() / "attachments"))


class TimelineService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx

    def feed(
        self,
        owner: str,
        *,
        actors: Optional[Sequence[str]] = None,
        tags: Optional[Sequence[str]] = None,
        mentions: Optional[str] = None,
        types: Optional[Sequence[str]] = None,
        since: Optional[str] = None,
        until: Optional[str] = None,
        limit: Optional[int] = None,
    ) -> List[EventDTO]:
        events = read_owner_events(self.ctx.memory_dir, owner, actors)
        events = self._apply_filters(events, tags=tags, mentions=mentions, types=types, since=since, until=until)
        events.sort(key=lambda e: e.created_at, reverse=True)
        if limit:
            events = events[:limit]
        return [self._serialize(e) for e in events]

    def get(self, owner: str, event_id: str) -> Optional[EventDTO]:
        events = read_owner_events(self.ctx.memory_dir, owner, None)
        for ev in events:
            if ev.id == event_id:
                return self._serialize(ev)
        return None

    def _apply_filters(
        self,
        events: List[Event],
        *,
        tags: Optional[Sequence[str]],
        mentions: Optional[str],
        types: Optional[Sequence[str]],
        since: Optional[str],
        until: Optional[str],
    ) -> List[Event]:
        filtered = events
        if tags:
            tagset = set(t.strip() for t in tags if t)
            filtered = [
                e
                for e in filtered
                if tagset.intersection(set(map(str.strip, (e.headers.get("tags", "").split(",")))))
            ]
        if mentions:
            needle = mentions.strip()
            filtered = [
                e
                for e in filtered
                if needle in e.headers.get("mentions", "") or needle in e.content
            ]
        if types:
            wanted = set(types)
            filtered = [e for e in filtered if e.type in wanted]
        since_dt = parse_iso(since)
        if since_dt:
            filtered = [e for e in filtered if parse_iso(e.created_at) and parse_iso(e.created_at) >= since_dt]
        until_dt = parse_iso(until)
        if until_dt:
            filtered = [e for e in filtered if parse_iso(e.created_at) and parse_iso(e.created_at) <= until_dt]
        return filtered

    def _serialize(self, event: Event) -> EventDTO:
        attachments = self._list_attachments(event.id)
        return EventDTO(
            id=event.id,
            type=event.type,
            owner=event.owner,
            actor=event.actor,
            created_at=event.created_at,
            headers=dict(event.headers),
            content=event.content,
            attachments=attachments,
        )

    def _list_attachments(self, event_id: str) -> List[str]:
        d = get_attachments_dir_for(event_id)
        if not d.exists():
            return []
        return sorted([p.name for p in d.iterdir() if p.is_file()])


class SocialService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx
        self.timeline = TimelineService(ctx)
        self._detector = MentionDetector()
        self._attachments = AttachmentService(ctx)

    def create_post(
        self,
        owner: str,
        actor: str,
        message: str,
        *,
        tags: Optional[List[str]] = None,
        mentions: Optional[List[str]] = None,
        attachments: Optional[Sequence[Union[Path, Tuple[str, Union[bytes, IO[bytes]]]]]] = None,
        auto_detect_mentions: bool = True,
    ) -> EventDTO:
        headers: Dict[str, str] = {}
        if tags:
            headers["tags"] = ",".join(tags)
        resolved_mentions = mentions or []
        if auto_detect_mentions and not resolved_mentions:
            resolved_mentions = self._detector.detect(message)
        if resolved_mentions:
            headers["mentions"] = " ".join(f"@{m}" for m in resolved_mentions)
        if attachments:
            headers["attachments"] = ",".join(self._attachment_names(attachments))
        ev = append_event(self.ctx.memory_dir, owner, actor, "post", message, headers)
        if attachments:
            self._attachments.save_uploads(ev.id, attachments)
        return self.timeline._serialize(ev)

    def create_comment(
        self,
        owner: str,
        actor: str,
        post_id: str,
        message: str,
        *,
        auto_detect_mentions: bool = True,
    ) -> EventDTO:
        headers = {"in_reply_to": post_id}
        mentions = self._detector.detect(message) if auto_detect_mentions else []
        if mentions:
            headers["mentions"] = " ".join(f"@{m}" for m in mentions)
        ev = append_event(self.ctx.memory_dir, owner, actor, "comment", message, headers)
        return self.timeline._serialize(ev)

    def create_reaction(self, owner: str, actor: str, post_id: str, reaction: str) -> EventDTO:
        headers = {"in_reply_to": post_id, "reaction": reaction}
        ev = append_event(self.ctx.memory_dir, owner, actor, "reaction", "", headers)
        return self.timeline._serialize(ev)

    def _attachment_names(
        self, attachments: Sequence[Union[Path, Tuple[str, Union[bytes, IO[bytes]]]]]
    ) -> List[str]:
        names: List[str] = []
        for item in attachments:
            if isinstance(item, Path):
                names.append(item.name)
            else:
                names.append(item[0])
        return names


class TaskService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx
        self.timeline = TimelineService(ctx)

    def list_tasks(
        self,
        owner: str,
        *,
        assignee: Optional[str] = None,
        status: Optional[str] = None,
    ) -> List[Dict[str, object]]:
        events = read_owner_events(self.ctx.memory_dir, owner, None)
        tasks = [e for e in events if e.type == "task"]
        out: List[Dict[str, object]] = []
        for task_ev in tasks:
            if assignee and assignee not in (task_ev.headers.get("assignees", "")):
                continue
            eff_status = self._effective_status(events, task_ev)
            if status and eff_status != status:
                continue
            out.append(
                {
                    "id": task_ev.id,
                    "title": task_ev.headers.get("title", ""),
                    "description": task_ev.content,
                    "status": eff_status,
                    "created_at": task_ev.created_at,
                    "actor": task_ev.actor,
                    "assignees": [a.strip() for a in task_ev.headers.get("assignees", "").split(",") if a.strip()],
                }
            )
        out.sort(key=lambda t: t["created_at"], reverse=True)
        return out

    def create_task(
        self,
        owner: str,
        actor: str,
        title: str,
        description: str,
        assignees: Optional[Sequence[str]] = None,
    ) -> EventDTO:
        assignees_csv = ",".join(assignees) if assignees else ""
        ev = task_create(self.ctx.memory_dir, owner, actor, title, description, assignees_csv)
        return self.timeline._serialize(ev)

    def update_status(self, owner: str, actor: str, task_id: str, status: str) -> Dict[str, object]:
        task_update_status(self.ctx.memory_dir, owner, actor, task_id, status)
        events = read_owner_events(self.ctx.memory_dir, owner, None)
        task_ev = next((e for e in events if e.id == task_id and e.type == "task"), None)
        if not task_ev:
            raise ValueError("Task not found")
        return {
            "id": task_id,
            "status": self._effective_status(events, task_ev),
        }

    def add_comment(self, owner: str, actor: str, task_id: str, message: str) -> EventDTO:
        ev = task_discuss(self.ctx.memory_dir, owner, actor, task_id, message)
        return self.timeline._serialize(ev)

    def _effective_status(self, events: List[Event], task_event: Event) -> str:
        status = task_event.headers.get("status", "open")
        updates = [e for e in events if e.headers.get("in_reply_to") == task_event.id and e.type == "task_update"]
        if not updates:
            return status
        updates.sort(key=lambda e: e.created_at)
        return updates[-1].headers.get("status", status)


class NotificationService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx

    def list(self, owner: str, target: str, last_hours: int = 24) -> List[Dict[str, str]]:
        return list_notifications(self.ctx.memory_dir, owner, target, last_hours)


class MentionService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx

    def list_mentions(
        self,
        owner: str,
        target: str,
        *,
        since: Optional[str] = None,
        until: Optional[str] = None,
    ) -> List[Dict[str, str]]:
        events = read_owner_events(self.ctx.memory_dir, owner, None)
        since_dt = parse_iso(since)
        until_dt = parse_iso(until)
        out: List[Dict[str, str]] = []
        needle = f"@{target}"
        for ev in events:
            ev_dt = parse_iso(ev.created_at)
            if since_dt and ev_dt and ev_dt < since_dt:
                continue
            if until_dt and ev_dt and ev_dt > until_dt:
                continue
            headers_mentions = ev.headers.get("mentions", "")
            if needle not in headers_mentions and needle not in ev.content:
                continue
            out.append(
                {
                    "id": ev.id,
                    "type": ev.type,
                    "actor": ev.actor,
                    "created_at": ev.created_at,
                    "snippet": ev.content[:280],
                }
            )
        out.sort(key=lambda e: e["created_at"], reverse=True)
        return out


class AttachmentService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx

    def list_for_event(self, event_id: str) -> List[str]:
        d = get_attachments_dir_for(event_id)
        if not d.exists():
            return []
        return sorted([p.name for p in d.iterdir() if p.is_file()])

    def open_file(self, event_id: str, file_name: str) -> Path:
        d = get_attachments_dir_for(event_id)
        target = d / file_name
        if not target.exists():
            raise FileNotFoundError(file_name)
        return target

    def save_uploads(
        self,
        event_id: str,
        uploads: Sequence[Union[Path, Tuple[str, Union[bytes, IO[bytes]]]]],
    ) -> None:
        dest = get_attachments_dir_for(event_id)
        for item in uploads:
            if isinstance(item, Path):
                if item.exists():
                    shutil.copy2(item, dest / item.name)
                continue
            name, payload = item
            target = dest / name
            if isinstance(payload, bytes):
                target.write_bytes(payload)
            else:
                with target.open("wb") as fh:
                    shutil.copyfileobj(payload, fh)


class AccountsService:
    def list_accounts(self):
        return load_accounts()


class SearchService:
    def __init__(self, ctx: ServiceContext):
        self.ctx = ctx

    def search(
        self,
        owner: str,
        query: str,
        *,
        actors: Optional[Sequence[str]] = None,
        limit: Optional[int] = 50,
    ) -> List[EventDTO]:
        events = read_owner_events(self.ctx.memory_dir, owner, actors)
        needle = query.lower()
        matched = [e for e in events if needle in e.content.lower() or needle in str(e.headers).lower()]
        matched.sort(key=lambda e: e.created_at, reverse=True)
        if limit:
            matched = matched[:limit]
        timeline = TimelineService(self.ctx)
        return [timeline._serialize(e) for e in matched]


class MentionDetector:
    """Utility to resolve mentions automatically for posts/comments."""

    def __init__(self):
        self._store = load_accounts()

    def detect(self, text: str) -> List[str]:
        known = list(self._store.accounts.keys())
        return extract_mentions(text, known)


__all__ = [
    "AccountsService",
    "AttachmentService",
    "EventDTO",
    "MentionDetector",
    "MentionService",
    "NotificationService",
    "SearchService",
    "ServiceContext",
    "SocialService",
    "TaskService",
    "TimelineService",
]
